Debugging is an aspect of the design and development of computer software that involves testing and evaluating the software to identify and correct errors in the software logic. Often a programmer will use another computer program and associated hardware, commonly known as a “debugger,” to debug software under development.
Conventional debuggers typically support two primary operations to assist a computer programmer. A first operation supported by conventional debuggers is a “step” function, which permits a computer programmer to process instructions (also known as “statements”) in a computer program one-by-one, and see the results upon completion of each instruction. While the step operation provides a programmer with a large amount of information about a program during its execution, stepping through hundreds or thousands of program instructions can be extremely tedious and time consuming, and may require a programmer to step through many program instructions that are known to be error-free before reaching a set of instructions to be analyzed.
To address this difficulty, a second operation supported by conventional debuggers is a breakpoint operation, which permits a computer programmer to identify with a “breakpoint” a precise instruction at which to halt execution of a computer program. As a result, when a debugger executes a computer program, the program executes in a normal fashion until it reaches a breakpoint and then stops. Once execution stops, a programmer can view a snapshot of the execution, including, for example, the program state and the value of variables. Further, upon reaching the breakpoint the programmer may step through the desired set of instructions one instruction at a time.
There are two conventional techniques for inserting breakpoints: (1) inserting the breakpoint into the software's object code, and (2) inserting a hardware breakpoint. Software breakpoints work by replacing a machine instruction with an emulation-trap instruction; when executed, this instruction causes the processor to vector to a routine that passes control to the debugger, which can then single-step the processor. Methods of inserting the breakpoint are well known in the art. For example, the debugger may simply write an instruction (e.g., an opcode) over the first byte or bytes of the target instruction that causes an interrupt to be fired whenever execution is transferred to the instruction's address. When this happens, the debugger “breaks in” and swaps the opcode byte with the original first byte of the instruction, so that the processor can continue execution without immediately hitting the same breakpoint.
Software breakpoints have limited usefulness, however, when programs are stored in shared memory, or the program is self-modifying. Shared memory is memory that may be simultaneously accessed by multiple programs, processors, or processing cores, sometimes to provide communication among the programs or conserve memory. Depending on the context, programs may run on a single processor or on multiple separate processors. If a breakpoint is inserted into code stored on shared memory and simultaneously executed by multiple cores, then any of the cores may encounter the breakpoint. If there are multiple debugging sessions active (e.g., each for a different core), the encountering session may not expect the breakpoint and will not have the original instruction saved, and therefore cannot continue execution. Further, if a programmer only intends to halt a single core, other cores may nevertheless halt and interfere with time critical behavior. Finally, when a debugger single-steps a core after halting, the debugger temporarily replaces the breakpoint with the original instruction; while the breakpoint is replaced another core may run through where the breakpoint should have been without halting.
There is another problem with debugging shared memory when two debuggers are active. Assume that two separate debuggers (Debuggers A and B) control two processors, and each debugger has the same breakpoint set internally. Debugger A reads the original instruction from shared memory and replaces it with the software breakpoint instruction. Debugger B reads the software breakpoint instruction instead of the original instruction and replaces the software breakpoint instruction with another software breakpoint instruction. When the processor under the control of Debugger B encounters the breakpoint and Debugger B begins step through of the breakpoint, Debugger B “restores” the breakpoint instruction with the erroneously stored breakpoint instruction.
Given the increased prevalence of multi-core processors, inserting software breakpoints into code stored on shared memory becomes even less feasible for debugging purposes.
One technique for inserting software breakpoints in shared memory is described in U.S. Pat. No. 6,990,657, SHARED SOFTWARE BREAKPOINTS IN A SHARED MEMORY SYSTEM. According to this technique, when a debug session sets a software breakpoint in shared memory, all active debug sessions are notified that the software breakpoint is set, and likewise notified when that software breakpoint is subsequently cleared. This approach still requires that every processor halt when any processor reaches a breakpoint and has a dramatic effect on run-time performance.
Another technique for managing software breakpoints in shared memory is described in U.S. Pat. No. 7,131,114, DEBUGGER BREAKPOINT MANAGEMENT IN A MULTICORE DSP DEVICE HAVING SHARED MEMORY. According to this technique, a DSP device comprising a shared memory is implemented in a host system. The host system debugs each of a plurality of processor subsystems in the DSP device and coupled to the shared memory. The host inserts a software breakpoint, and when a subsystem trips a breakpoint, the host determines whether the breakpoint is associated with the subsystem and if not, causes the subsystem to execute the original software instruction. This approach requires that all subsystems that have a breakpoint associated with the triggered location halt, and dramatically effects the run-time performance of processors.
Hardware breakpoints are more powerful and flexible than software breakpoints. Unlike software breakpoints, hardware breakpoints may set “memory breakpoints”, or a breakpoint that is fired when any instruction attempts to read, write, or execute (depending on the breakpoint is configured) a specific address. There is also support for setting breakpoints on I/O port access. Hardware breakpoints have some limitations, however; the main limit being that the number of active hardware breakpoints is typically small. For example, on a typical x86 microprocessor, only four hardware breakpoints may be active at the same time.
Self-modifying software instructions also pose a problem for debugging systems. Self-modifying code is code that alters its own instructions while executing—perhaps to reduce the instruction path length and improve performance, or simply to reduce otherwise repetitively similar code. Self-modification is an alternative to the method of “flag setting” and conditional program branching used primarily to reduce the number of times a condition needs to be tested. Self-modifying code is straightforward to implement when using assembly language. Instructions can be dynamically created in memory (or else overlaid over existing code in non-protected program storage) in a sequence equivalent to the instructions that a standard compiler would generate as object code.
When a breakpoint is inserted into self-modifying program code, there is always the possibility that the self-modifying code will overwrite the breakpoint. The same drawbacks for hardware breakpoints used with self-modifying program code exist as with shared memory.
Thus, there is a need for a technique for inserting breakpoints in shared memory that has the flexibility of hardware breakpoints, but with the availability of software breakpoints that also addresses the issues presented by self-modifying code.